﻿using DevExpress.Data.Filtering;
using DevExpress.Data.Filtering.Helpers;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Data;
using DevExpress.ExpressApp.DC;
using DevExpress.Persistent.Base;
using System.Collections;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace EasyXaf.ObjectSpaces.OData;

[DomainComponent]
[Microsoft.OData.Client.EntityType]
public abstract class ODataObject : IObjectSpaceLink, INotifyPropertyChanged, IDisposable
{
    private readonly ConcurrentDictionary<string, object> PropertyStore = new();
    private readonly ConcurrentDictionary<string, IMemberInfo> MemberInfoStore = new();

    private bool _loading = false;

    private ITypeInfo _typeInfo;

    protected IODataObjectSpace ObjectSpace => (IODataObjectSpace)((IObjectSpaceLink)this).ObjectSpace;

    protected Microsoft.OData.Client.DataServiceContext ODataContext => ObjectSpace?.ODataContext;

    protected ITypeInfo TypeInfo => _typeInfo ??= ObjectSpace.TypesInfo.FindTypeInfo(GetType());

    protected bool IsLoading => _loading || ObjectSpace == null;

    IObjectSpace IObjectSpaceLink.ObjectSpace { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private IMemberInfo GetMemberInfo(string propertyName)
    {
        if (!MemberInfoStore.TryGetValue(propertyName, out var memberInfo))
        {
            memberInfo = TypeInfo.FindMember(propertyName);
            MemberInfoStore[propertyName] = memberInfo;
        }
        return memberInfo;
    }

    private static bool CanSkipAssignment(object oldValue, object newValue)
    {
        if (oldValue == newValue)
        {
            return true;
        }

        if (oldValue is ValueType && newValue is ValueType && Equals(oldValue, newValue))
        {
            return true;
        }

        if (oldValue is string && newValue is string && Equals(oldValue, newValue))
        {
            return true;
        }

        return false;
    }

    protected T GetPropertyValue<T>([CallerMemberName] string propertyName = null)
    {
        if (!IsLoading && !MemberInfoStore.ContainsKey(propertyName))
        {
            var memberInfo = GetMemberInfo(propertyName);
            if (memberInfo.IsAssociation && memberInfo.IsDelayed)
            {
                _loading = true;

                ODataContext.LoadProperty(this, propertyName);

                _loading = false;
            }
        }

        if (PropertyStore.TryGetValue(propertyName, out var value))
        {
            if (typeof(ODataObject).IsAssignableFrom(typeof(T)))
            {
                ObjectSpace.AcceptObject(value);
            }

            return (T)value;
        }

        return default;
    }

    protected bool SetPropertyValue<T>(T newValue, [CallerMemberName] string propertyName = null)
    {
        var oldValue = GetPropertyValue<T>(propertyName);
        if (CanSkipAssignment(oldValue, newValue))
        {
            return false;
        }

        if (newValue == null)
        {
            PropertyStore.TryRemove(propertyName, out _);
        }
        else
        {
            PropertyStore[propertyName] = newValue;
        }

        if (!IsLoading && typeof(ODataObject).IsAssignableFrom(typeof(T)))
        {
            if (oldValue != null)
            {
                ODataContext.DetachLink(this, propertyName, oldValue);

                var memberInfo = GetMemberInfo(propertyName);
                if (memberInfo.AssociatedMemberInfo != null)
                {
                    if (memberInfo.AssociatedMemberInfo.IsList)
                    {
                        if (memberInfo.AssociatedMemberInfo.GetValue(oldValue) is IList list)
                        {
                            list.Remove(this);
                        }
                    }
                    else
                    {
                        memberInfo.AssociatedMemberInfo.SetValue(oldValue, null);
                    }
                }
            }

            if (newValue != null)
            {
                var linkDescriptor = ODataContext.GetLinkDescriptor(this, propertyName, newValue);
                if (linkDescriptor == null)
                {
                    ODataContext.SetRelatedObjectLink(this, propertyName, newValue);
                }

                var memberInfo = GetMemberInfo(propertyName);
                if (memberInfo.AssociatedMemberInfo != null)
                {
                    if (memberInfo.AssociatedMemberInfo.IsList)
                    {
                        if (memberInfo.AssociatedMemberInfo.GetValue(newValue) is IList list)
                        {
                            list.Add(this);
                        }
                    }
                    else
                    {
                        memberInfo.AssociatedMemberInfo.SetValue(newValue, this);
                    }
                }
            }
        }

        OnPropertyChanged(propertyName);

        return true;
    }

    protected IList<T> GetCollection<T>(CriteriaOperator criteria = null, [CallerMemberName] string propertyName = null)
    {
        if (!PropertyStore.TryGetValue(propertyName, out var result) && !IsLoading)
        {
            if (criteria.ReferenceEqualsNull())
            {
                var typeInfo = ObjectSpace.TypesInfo.FindTypeInfo(GetType());
                var memberInfo = typeInfo.FindMember(propertyName);
                if (memberInfo.AssociatedMemberInfo != null)
                {
                    criteria = new BinaryOperator($"{memberInfo.AssociatedMemberInfo.Name}.Id", ObjectSpace.GetKeyValue(this));
                }
            }
            var collection = ObjectSpace.CreateCollection<T>(criteria, this, propertyName);
            PropertyStore[propertyName] = collection;
            return collection;
        }
        return (IList<T>)result;
    }

    public void Reload()
    {
        ODataContext.Detach(this);

        var key = ObjectSpace.GetKeyValue(this);
        var typeInfo = ObjectSpace.TypesInfo.FindTypeInfo(GetType());

        var newObject = ObjectSpace
             .GetObjectsQuery(GetType(), new BinaryOperator(typeInfo.KeyMember.Name, key))
             .OfType<ODataObject>()
             .First();

        PropertyStore.Clear();

        foreach (var propertyName in newObject.PropertyStore.Keys)
        {
            PropertyStore[propertyName] = newObject.PropertyStore[propertyName];
        }

        ODataContext.Detach(newObject);
        ODataContext.AttachTo(GetType().GetEntitySetName(), this);
    }

    public void Dispose()
    {
        PropertyStore.Clear();
        MemberInfoStore.Clear();

        PropertyChanged = null;
        ((IObjectSpaceLink)this).ObjectSpace = null;

        GC.SuppressFinalize(this);
    }
}

[DomainComponent]
[Microsoft.OData.Client.EntityType]
[Microsoft.OData.Client.Key(nameof(Id))]
public abstract class ODataObject<TKey> : ODataObject
{
    private TKey _id;

    [Key]
    [VisibleInDetailView(false)]
    [VisibleInListView(false)]
    [VisibleInLookupListView(false)]
    public TKey Id
    {
        get => _id;
        set => _id = value;
    }

    public override bool Equals(object obj)
    {
        if (obj == null)
        {
            return false;
        }

        if (ReferenceEquals(this, obj))
        {
            return true;
        }

        return ((ODataObject<TKey>)obj).Id.Equals(Id);
    }

    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}